﻿/*
Copyright (c) 2010 Lance Ward
This software is licensed under the MIT license, except where otherwise noted.
For complete license details, refer to license.txt
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace ADNLib
{
    [Serializable]
	public class ObjectPath
	{
		private List<PathPoint> points = new List<PathPoint>();

		[XmlElement("PATHPOINT")]
		public List<PathPoint> Points
		{
			get
			{
				return points;
			}
			set
			{
				points = value;
			}
		}

		public PointF[] GetPoints()
		{
			List<PointF> p = new List<PointF>();

			for (int i = 0; i < points.Count; i++)
			{
				p.Add(points[i].Point);
			}

			return p.ToArray();
		}

		public PointF[] GetAControlPoints()
		{
			List<PointF> p = new List<PointF>();

			for (int i = 0; i < points.Count; i++)
			{
				p.Add(points[i].ControlA);
			}

			return p.ToArray();
		}

		public PointF[] GetBControlPoints()
		{
			List<PointF> p = new List<PointF>();

			for (int i = 0; i < points.Count; i++)
			{
				p.Add(points[i].ControlB);
			}

			return p.ToArray();
		}

        public PointF[] GetBezierPoints()
        {
            //control points should be as long as points

            List<PointF> p = new List<PointF>();
            p.Add(points[0].Point); //add initial point

            for (int i = 0; i < points.Count - 1; i++)
            {
                p.Add(points[i].ControlB);
                p.Add(points[i + 1].ControlA);
                p.Add(points[i + 1].Point);
            }

            return p.ToArray();
        }

        public PointF GetPositionAlongPath(float movePercent)
        {
			//measure the path
			GraphicsPath gp = new GraphicsPath();
			gp.AddBeziers(GetBezierPoints());

			float totalLen = MeasurePath(gp);
			float pos = totalLen * movePercent;

			//find the point that represents this value
			return GetPositionPoint(gp, pos);
        }

		private PointF GetPositionPoint(GraphicsPath flatPath, float pos)
		{
			float totalDist = 0.0f;

			for (int i = 0; i < flatPath.PathPoints.Length - 1; i++)
			{
				float lineLen = MeasureLine(flatPath.PathPoints[i], flatPath.PathPoints[i + 1]);
				totalDist += lineLen;
				if (totalDist > pos)
				{
					float percent = (totalDist - pos) / lineLen;
					return GetFractionalPoint(flatPath.PathPoints[i], flatPath.PathPoints[i + 1], percent);
				}
			}

			return new PointF(0, 0);
		}

		public PointF GetFractionalPoint(PointF a, PointF b, float percent)
		{
			float xDiff = b.X - a.X;
			float yDiff = b.Y - a.Y;

			return new PointF(a.X + (xDiff * percent), a.Y + (yDiff * percent));
		}

        private float MeasurePath(GraphicsPath gp)
        {
	        //.25 is the default. - reduce for a higher degree of accuracy - null because we want the untransformed values
	        gp.Flatten(null, 0.25f);
	        float totalDist = 0.0f;
        	
	        for(int i = 0; i < gp.PathPoints.Length - 1; i++)
	        {
				totalDist += MeasureLine(gp.PathPoints[i], gp.PathPoints[i + 1]);
	        }
        	
	        return totalDist;
        }

        private float MeasureLine(PointF a, PointF b)
        {
            float diff = (float)Math.Pow(a.X - b.X, 2) + (float)Math.Pow(a.Y - b.Y, 2);
            return (float)Math.Sqrt(diff);
        }

        public PathPoint DivideBezier(PointF location, PathPoint start, PathPoint end)
        {
            //subdivide bezier by 100
            List<PointF> pts = SubDivideBezier(100, start, end);
            //find index of location - between point A & point B
            int index = -1;

            for (int i = 0; i < pts.Count - 1; i++)
            {
                if (HitTestLine(pts[i], pts[i + 1], location)) { index = i; }
            }

            //find the difference between a & location
            //find the length of a to b
            float percent = MeasureLine(pts[index], location) / MeasureLine(pts[index], pts[index + 1]);

            //mult by percent of ab segment needed
            //find t from index. t = index / 99; t1 = index + 1/99;
            float t1 = (float)index / 99.0f;
            float t2 = ((float)index + 1.0f) / 99.0f;
            float t = t1 + ((t2 - t1) * percent);
            //mult t by same percent
            //get control point using t
            PointF[] bpts = SplitBezier(t, start, end);
            PathPoint bezPt = new PathPoint(bpts[2]);
            bezPt.ControlA = bpts[1];
            bezPt.ControlB = bpts[3];
 
            start.ControlB = bpts[0];
            end.ControlA = bpts[4];

            return bezPt;
        }

        /*
        http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
        Return values
        pt[0] = new control B for prev point
        pt[0] = new control point A for inserted
        pt[1] = new point to insert
        pt[2] = new control point B for inserted point
        pt[4] = new control A for next point
        */

        private PointF[] SplitBezier(float t, PathPoint start, PathPoint end)
        {
            PointF pA = start.Point;
            PointF pB = end.Point;
            PointF cA = start.ControlB;
            PointF cB = end.ControlA;

            PointF subA = LI(pA, cA, t);
            PointF subB = LI(cA, cB, t);
            PointF subC = LI(cB, pB, t);
            PointF subAB = LI(subA, subB, t);
            PointF subBC = LI(subB, subC, t);
            return new PointF[5] { subA, subAB, LI(subAB, subBC, t), subBC, subC };

        }

        private List<PointF> SubDivideBezier(int divisions, PathPoint start, PathPoint end)
        {
            List<PointF> points = new List<PointF>();

            for (int i = 0; i < divisions; i++)
            {
                float t = i / (float)(divisions - 1);
                PointF[] pts = SplitBezier(t, start, end);
                points.Add(pts[2]);
            }

            return points;
        }

        // http://en.wikipedia.org/wiki/Linear_interpolation
        private PointF LI(PointF a, PointF b, float t) { return LinearInterpolation(a, b, t); }
        private PointF LinearInterpolation(PointF a, PointF b, float t)
        {
            PointF dest = new PointF();
            dest.X = a.X + (b.X - a.X) * t;
            dest.Y = a.Y + (b.Y - a.Y) * t;

            return dest;
        }

        private bool HitTestLine(PointF a, PointF b, PointF location)
        {
            GraphicsPath gp = new GraphicsPath();
            gp.AddLine(a, b);
            return gp.IsOutlineVisible(location, new Pen(Color.Black, 4.0f));
        }

	}
}
